/**
 * 20211118 created by jordia
 * 20211130:增加/修正碰撞总和方向为0向量时的处理方式
 * 20211201:修改
 * 20220215:增加质点重复添加的警告
 * 20220304:[require] to [import]...jordia
 */
import {MeshBasicMaterial,Vector3,Matrix4,Mesh} from 'three';
import Face3 from "../compute/face3";
import Utils from "../general/utils";

const HIDDEN_MATERIAL=new MeshBasicMaterial({visible:false});
const RAND_LIST=new Int8Array([
    4,6,-6, 5,4,2, -1,-3,4, 8,4,7, -9,6,9, 8,3,-1, 2,-1,-3, -2,-1,-5, -4,8,4, -7,7,-8, 4,7,8,
    -3,-1,-5, -6,-1,3, 1,8,4, -5,1,6, 4,5,4, 6,1,8, -9,7,9, 2,-7,0, 3,-6, -5, -7,-5,9, -5,9,-1,
    9,5,3, 0,9,2, 1,8,6, -1,-1,7, -3, 8,-1, 9,3,-2, -6,1,1, 7,9, -3, -1,-0,-5, -1,  -1,-8, 5,4,8,
    -0,7,4, -4,-6,2, 3,7,-9, 9, -6,2, 7,4,9, -5,6,-7, -3,5, 1, 8,8,-5, 7,5,2, 7,-2,-4, 8,-9,-1,
    2,2,  -7, 9,-3,-8, -1, -8,-3, -0,-1,-1]);
const EQUAL=(a,b)=>{
    let na=a.normal;
    let nb=b.normal;
    let x=na.x-nb.x;
    let y=na.y-nb.y;
    let z=na.z-nb.z;
    return x*x+y*y+z*z<1e-6;
};

/**
 * 凸壳边界面
 */
class FaceClipper{
    constructor(mesh){
        this.mesh=mesh;
        this.faces=[];
        this.cents=[];
        this._ta=new Vector3();
        this._tb=new Vector3();
        this._tc=new Vector3();
        this._td=new Vector3();
    }
    createFace(a,b,c){
        let face=new Face3(a,b,c);
        face.update();
        for(let f of this.faces){
            if(EQUAL(f,face))return;
        }
        this.cents.push(face.sphere.center);
        this.faces.push(face);
    }
    initialize(){
        let g=this.mesh.geometry;
        let pos=g.attributes['position'].array;
        if(g.index){
            let idx=g.index.array;
            for(let i=0;i<idx.length;i+=3){
                let ai=idx[i]*3,bi=idx[i+1]*3,ci=idx[i+2]*3;
                this._ta.set(pos[ai],pos[ai+1],pos[ai+2]);
                this._tb.set(pos[bi],pos[bi+1],pos[bi+2]);
                this._tc.set(pos[ci],pos[ci+1],pos[ci+2]);
                this.createFace(this._ta,this._tb,this._tc);
            }
        }else{
            for(let i=0;i<pos.length;i+=9){
                this._ta.set(pos[i],pos[i+1],pos[i+2]);
                this._tb.set(pos[i+3],pos[i+4],pos[i+5]);
                this._tc.set(pos[i+6],pos[i+7],pos[i+8]);
                this.createFace(this._ta,this._tb,this._tc);
            }
        }
    }
    checkPoint(mp){
        let p=this._ta.copy(mp.position);
        let sus=0,d=this._td;
        let maxDepth=0;
        for(let i=0;i<this.faces.length;i++) {
            let face = this.faces[i];
            let cent=this.cents[i];
            let n=face.normal;
            this._tb.copy(n);
            this._tc.subVectors(p, cent);
            if (n.dot(this._tc) < 0) continue;
            let l=this._tc.projectOnVector(n).lengthSq();
            if(maxDepth<l)maxDepth=l;
            d.copy(n);
            sus++;
        }
        if(sus){
            mp._count++;
            mp._lastNormal.copy(d);
            mp._overStep++;
        }
        return Math.sqrt(maxDepth);
    }
    dispose(){
        this.faces.length=0;
        this.cents.length=0;
    }
}

/**
 * 简化的轴扫掠粗碰撞对生成
 */
class SweepAxis{
    constructor(){
        this.pairs=[];
        this.xPairs=[];
        this.xyPairs=[];
        this.xyzPairs=[];
        this.tA0=[];
    }
    collectOverlapPairs(ps){
        this.xyzPairs.length=0;
        if(!ps.length)return this.xyzPairs;
        this.checkXAxis(ps);
        if(!this.xPairs.length)return this.xyzPairs;
        this.checkYAxis(ps);
        if(!this.xyPairs.length)return this.xyzPairs;
        this.checkZAxis(ps);
        return this.xyzPairs;
    }
    checkZAxis(mps){
        for(let i=0;i<this.xyPairs.length;i++){
            let pa=mps[this.xyPairs[i][0]],pb=mps[this.xyPairs[i][1]];
            let az=pa.position.z,bz=pb.position.z;
            if(Math.abs(az-bz)<(pa.margin+pb.margin))this.xyzPairs.push(this.xyPairs[i]);
        }
    }
    checkYAxis(mps){
        this.xyPairs.length=0;
        for(let i=0;i<this.xPairs.length;i++){
            let pa=mps[this.xPairs[i][0]],pb=mps[this.xPairs[i][1]];
            let ay=pa.position.y,by=pb.position.y;
            if(Math.abs(ay-by)<(pa.margin+pb.margin))this.xyPairs.push(this.xPairs[i]);
        }
    }
    checkXAxis(mps){
        this.xPairs.length=0;
        let xs=this.tA0;
        xs.length=0;
        for(let i=0;i<mps.length;i++){
            xs.push(mps[i].position.x);
        }
        let iAry=Utils.sortIndexArray(xs);
        for(let i=0;i<iAry.length;i++){
            for(let j=i+1;j<iAry.length;j++){
                let h=iAry[i],k=iAry[j];
                let cp=mps[h],np=mps[k];
                if(Math.abs(xs[h]-xs[k])<(cp.margin+np.margin)){
                    this.xPairs.push([h,k]);
                }else{
                    break;
                }
            }
        }
    }
}


/**
 * 质点（容器坐标系）
 * @param obj {Object3D} 所绑定的3D对象
 * @param container {Object3D} 凸壳容器
 * @param values {object} 可选参数对象
 *      {
 *          direction:粒子运动向量（世界）,
 *          margin:质点范围半径
 *      }
 *
 * @property object {Object3D} 所绑定的3D对象
 * @property type {string} 类型标识
 * @property onImpulse {function} 碰撞回调函数(targets:与之碰撞的对象序列)=>{}
 * @property direction {vector3} 粒子运动向量（局部）
 * @property margin {number} 质点范围半径
 * @property position {vector3} 质点位置（局部）
 */
class MassPoint {
    constructor(obj,container,values={}){
        this._type='massPoint';
        this._object=obj;
        this._container=container;
        this._initialize(values);
    }

    //................................................public......................................................

    get object(){
        return this._object;
    }
    get type(){
        return this._type;
    }
    set onImpulse(func_targets){
        this._impulseCallback=func_targets;
    }
    get onImpulse(){
        return this._impulseCallback;
    }
    get impulseTargets(){
        return this._impulseTargets;
    }
    get direction(){
        return this._direction;
    }
    set direction(vec){
        this._direction.copy(vec);
    }
    get margin(){
        return this._margin;
    }
    set margin(num){
        this._margin=num;
    }
    get position(){
        return this._position;
    }
    set position(vec){
        this._position.copy(vec);
        this._synchronize();
    }

    //.................................................private....................................................

    _initialize(values){
        this._tM0=new Matrix4().copy(this._container.matrixWorld).invert();
        this._direction=values.direction||new Vector3(0,1,0);
        this._margin=values.margin||0.01;
        this._tV0=new Vector3(0,0,0).applyMatrix4(this._object.matrixWorld);
        this._tV0.applyMatrix4(this._tM0);
        this._position=new Vector3().copy(this._tV0);
        this._tM0.extractRotation(this._tM0);
        this._direction.applyMatrix4(this._tM0);
        this._normal=new Vector3(0,0,0);
        this._count=0;
        this._lastNormal=new Vector3(0,0,0);
        this._overStep=0;
        this._impulseCallback=null;
        this._impulseTargets=[];
    }
    _stepIn(dt){
        this._position.add(this._tV0.copy(this._direction).multiplyScalar(dt));
    }
    _synchronize(){
        this._tV0.copy(this._position);
        this._tV0.applyMatrix4(this._container.matrixWorld);
        if(this._object.parent){
            this._tM0.copy(this._object.parent.matrixWorld).invert();
            this._tV0.applyMatrix4(this._tM0);
        }
        this._object.position.copy(this._tV0);
    }
    dispose(){
        this._impulseTargets.length=0;
    }
}

/**
 * 凸壳容器内质点碰撞模拟（速度恒定）
 * @param container {Mesh} 容器网格对象，必须是凸壳封闭的，且法线指向外侧
 * @param values {object} 可选参数对象
 *      {
 *          normalOffset:0.2,与容器碰撞后未立即脱离接触时的修正值,
 *          enableMassPointCollide:false,启用质点之间的碰撞,
 *          worldSpace:false,使用世界空间
 *      }
 *
 * @property container {mesh} 容器网格对象，必须是凸壳封闭的，且法线指向外侧
 * @property normalOffset {number} 与容器碰撞后未立即脱离接触时的修正值
 * @property enableMassPointCollide {Boolean} 是否启用质点之间的碰撞
 * @property onContainerImpulse {function} 容器碰撞的回调函数(container:容器对象,massPoints:质点序列,massPointIndices:与容器碰撞的质点索引)=>{}
 */
export default class MassPointConvexContainer {
    constructor(container=null,values={}){
        this._container=null;
        this._init(values);
        this.container=container;
    }

    //....................................................public.........................................................

    set container(mesh){
        if(this._faceClipper)this._faceClipper.dispose();
        if(mesh&&mesh.geometry){
            if(this._worldSpace){
                if(this._container)this._container.geometry.dispose();
                this._container=new Mesh(mesh.geometry.clone().applyMatrix4(mesh.matrixWorld),HIDDEN_MATERIAL);
            }else{
                this._container=mesh;
            }

            if(this._faceClipper){
                this._faceClipper.mesh=this._container;
            }else{
                this._faceClipper=new FaceClipper(this._container);
            }
            this._faceClipper.initialize();
        }else{
            if(this._worldSpace){
                if(this._container)this._container.geometry.dispose();
            }
            this._container=null;
        }
    }
    get container(){
        return this._container;
    }
    set normalOffset(num){
        this._normalOffset=Math.max(num,1e-2);
    }
    get normalOffset(){
        return this._normalOffset;
    }
    set enableMassPointCollide(bol){
        this._enableMassPointCollide=bol;
    }
    get enableMassPointCollide(){
        return this._enableMassPointCollide;
    }
    set onContainerImpulse(func_container_massPoints_massPointIndices){
        this._containerCallback=func_container_massPoints_massPointIndices;
    }

    /**
     * 添加质点
     * @param obj {Object3D} 与质点绑定的3D对象
     * @param margin {number} 质点的范围半径
     * @param direction {vector} 质点的起始运动向量
     */
    addMassPoint(obj,margin,direction) {
        if(!this._container){
            console.warn('[.container] needs to be settled before add any mass point!');
            return;
        }
        let idx = this._points.findIndex((c) => c.object.uuid === obj.uuid);
        if (idx < 0) {
            let mp=new MassPoint(obj,this._container,{margin:margin,direction:direction});
            obj.userData.massPoint=mp;
            this._points.push(mp);
            return mp;
        }
        console.warn('can only add a mass-point for once!',obj);
        return null;
    }

    /**
     * 移除质点
     * @param obj {Object3D} 与质点绑定的3D对象
     */
    removeMassPoint(obj) {
        let idx = this._points.findIndex((c) => c.object.uuid === obj.uuid);
        if (idx < 0) return;
        obj.userData.massPoint.dispose();
        obj.userData.massPoint=undefined;
        this._points.splice(idx, 1);
    }

    /**
     * 计算模拟
     * @param dt {number} 以秒计的间隔时间
     */
    stepIn(dt){
        for(let mp of this._points){
            mp._count=0;
            mp._normal.set(0,0,0);
            mp._stepIn(dt);
            mp.impulseTargets.length=0;
        }
        if(this._enableMassPointCollide)this._applyMassPointImpulse();
        this._applyContainerImpulse();
        for(let mp of this._points){
            mp._synchronize();
            if(mp.onImpulse){
                mp.onImpulse(mp.impulseTargets);
            }
        }
    }

    //....................................................private.......................................................

    _applyMassPointImpulse(){
        let pairs=this._broadPhase.collectOverlapPairs(this._points);
        let n=this._tV0;
        for(let i=0;i<pairs.length;i++){
            let pa=this._points[pairs[i][0]],pb=this._points[pairs[i][1]];
            let posA=pa.position,posB=pb.position;
            if(posA.distanceToSquared(posB)<(pa.margin+pb.margin)){
                n.subVectors(posB,posA).normalize();
                pa._normal.add(n);
                pa._count++;
                pb._normal.sub(n);
                pb._count++;
                pa.impulseTargets.push(pb);
                pb.impulseTargets.push(pa);
            }
        }
    }
    _applyContainerImpulse(){
        this._impulseIndices.length=0;
        for(let i=0;i<this._points.length;i++){
            let mp=this._points[i];
            let dph=this._faceClipper.checkPoint(mp);
            if(dph){
                mp.impulseTargets.push(this._container);
                this._impulseIndices.push(i);
            }else{
                mp._overStep=0;
            }
            if(mp._count){
                let n=dph?mp._lastNormal:mp._normal.divideScalar(mp._count);
                if(n.x*n.x+n.y*n.y+n.z*n.z<1e-6){
                    n.set(this._rand(),this._rand(),this._rand()).normalize();
                }else{
                    n.normalize();
                }
                mp.direction.reflect(n);
                if(dph){
                    dph+=mp.margin*this._normalOffset;
                    let dir=mp.direction;
                    let len=dir.length();
                    mp.position.add(n.multiplyScalar(-dph));
                    if(dir.x*dir.x+dir.y*dir.y+dir.z*dir.z<1e-6){
                        dir.set(this._rand(),this._rand(),this._rand());
                    }
                    dir.normalize().multiplyScalar(len);
                }else{
                    if(mp.direction.dot(n)>0){
                        mp.direction.negate();
                    }
                }
            }
        }
        if(this._containerCallback)this._containerCallback(this._container,this._points,this._impulseIndices);
    }
    _init(values){
        this._initVariable(values);
    }
    _initVariable(values){
        this._points=[];
        this._faceClipper=null;
        this._normalOffset=values['normalOffset']||0.2;
        this._broadPhase=new SweepAxis();
        this._tV0=new Vector3();
        this._tV1=new Vector3();
        this._containerCallback=null;
        this._impulseIndices=[];
        this._enableMassPointCollide=values['enableMassPointCollide']||false;
        this._worldSpace=values['worldSpace']||false;
        this._randT=0;
    }
    _rand(){
        this._randT++;
        this._randT%=RAND_LIST.length;
        return RAND_LIST[this._randT];
    }
}